﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;


namespace advadev.CanOpen
{

    /// <summary>
    /// Represents a multiplex process data object write transfer
    /// </summary>
    public class MultiplexProcessDataObjectWriteTransfer : ITransfer
    {

        /// <summary>for internal use</summary>
        private bool _isProducerTransfer;
        /// <summary>for internal use</summary>
        private bool _isTransferCompleteAfterSending;
        /// <summary>for internal use</summary>
        private uint _mpdoIndex;
        /// <summary>for internal use</summary>
        private CanOpenMessage _receivedCanOpenMessage;
        /// <summary>for internal use</summary>
        private ICanOpenObjectDictionaryContainer _canOpenObjectDictionaryContainer;
        /// <summary>for internal use</summary>
        private Collection<CanOpenMessage> _pendingSendMessages = new Collection<CanOpenMessage>();
        /// <summary>for internal use</summary>
        private ReadOnlyCollection<CanOpenMessage> _pendingSendMessagesReference;
        /// <summary>
        /// Default PDO 1 transmit COB-ID
        /// </summary>
        static public readonly UInt16 DefaultPDO1TransmitCobId = 0x180;
        /// <summary>
        /// Default PDO 2 transmit COB-ID
        /// </summary>
        static public readonly UInt16 DefaultPDO2TransmitCobId = 0x280;
        /// <summary>
        /// Default PDO 3 transmit COB-ID
        /// </summary>
        static public readonly UInt16 DefaultPDO3TransmitCobId = 0x380;
        /// <summary>
        /// Default PDO 4 transmit COB-ID
        /// </summary>
        static public readonly UInt16 DefaultPDO4TransmitCobId = 0x480;
        /// <summary>
        /// Default PDO 1 receive COB-ID
        /// </summary>
        static public readonly UInt16 DefaultPDO1ReceiveCobId = 0x200;
        /// <summary>
        /// Default PDO 2 receive COB-ID
        /// </summary>
        static public readonly UInt16 DefaultPDO2ReceiveCobId = 0x300;
        /// <summary>
        /// Default PDO 3 receive COB-ID
        /// </summary>
        static public readonly UInt16 DefaultPDO3ReceiveCobId = 0x400;
        /// <summary>
        /// Default PDO 4 receive COB-ID
        /// </summary>
        static public readonly UInt16 DefaultPDO4ReceiveCobId = 0x500;


        /// <summary>
        /// Initializes a new instance of the
        /// MultiplexProcessDataObjectWriteTransfer class by the given parameter
        /// </summary>
        /// <param name="receivedCanOpenMessage">
        /// Received can open message
        /// </param>
        /// <param name="canOpenObjectDictionaryContainer">
        /// Can open object dictionary container
        /// </param>
        /// <param name="mpdoIndex">
        /// MPDO index
        /// </param>
        private MultiplexProcessDataObjectWriteTransfer(CanOpenMessage receivedCanOpenMessage, ICanOpenObjectDictionaryContainer canOpenObjectDictionaryContainer, uint mpdoIndex)
        {
            _isProducerTransfer = false;
            _mpdoIndex = mpdoIndex;
            _receivedCanOpenMessage = receivedCanOpenMessage;
            _canOpenObjectDictionaryContainer = canOpenObjectDictionaryContainer;
        }


        /// <summary>
        /// Initializes a new instance of the
        /// MultiplexProcessDataObjectWriteTransfer class by the given parameter
        /// </summary>
        /// <param name="canOpenValueObjects">
        /// Can open value object to send in the transfer (if the size of the
        /// given can open value object is greater than 4 bytes the Start method
        /// will throw a exception)
        /// </param>
        /// <param name="addressType">
        /// Address type to use
        /// </param>
        /// <param name="nodeId">
        /// Node id of the device that writes the MPDO (must be in the range of
        /// 1..127)
        /// </param>
        /// <param name="destinationNodeId">
        /// Node id of the device to write the MPDO to (must be in the range of
        /// 0..127; only used for address type "DestinationAddressing"; 0 is for
        /// group addressing)
        /// </param>
        /// <param name="cobId">
        /// COB-ID
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Will be thrown if the can open value object is null
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Will be thrown if the node id is not in the range of 1..127 or if
        /// the destination node id is not in the range of 0..127 for
        /// destination addressing
        /// </exception>
        public MultiplexProcessDataObjectWriteTransfer(ICanOpenValueObject canOpenValueObject, MultiplexProcessDataObjectAdressType addressType, byte nodeId, byte destinationNodeId, UInt16 cobId)
        {
            // check parameter
            if (canOpenValueObject == null)
            {
                throw new ArgumentNullException("canOpenValueObject");
            }
            if (!(1 <= nodeId && nodeId <= 127))
            {
                throw new ArgumentOutOfRangeException("nodeId", "The node id must be in the range of 1..127");
            }
            if (addressType == MultiplexProcessDataObjectAdressType.DestinationAddressing)
            {
                if (!(0 <= destinationNodeId && destinationNodeId <= 127))
                {
                    throw new ArgumentOutOfRangeException("destinationNodeId", "The destination node id must be in the range of 0..127");
                }
            }

            // initializaton
            _isProducerTransfer = true;
            NodeId = nodeId;
            DestinationNodeId = destinationNodeId;
            CobId = cobId;
            AddressType = addressType;
            CanOpenValueObject = canOpenValueObject;
        }


        /// <summary>
        /// Gets or sets the node id
        /// </summary>
        private byte NodeId
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the destination node id
        /// </summary>
        private byte DestinationNodeId
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the COB-ID
        /// </summary>
        private UInt16 CobId
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the address type
        /// </summary>
        private MultiplexProcessDataObjectAdressType AddressType
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the can open value object
        /// </summary>
        private ICanOpenValueObject CanOpenValueObject
        {
            get;
            set;
        }


        /// <summary>
        /// Checks if the given received can open message is a MPDO write
        /// transfer request and if so it returns as out parameter a matching
        /// MPDO write transfer instance
        /// </summary>
        /// <param name="receivedCanOpenMessage">
        /// Received can open message
        /// </param>
        /// <param name="nodeId">
        /// Node id of the device that received the MPDO (must be in the range
        /// of 0..127; 0 is valid for master)
        /// </param>
        /// <param name="canOpenObjectDictionaryContainer">
        /// Can open object dictionary container
        /// </param>
        /// <param name="multiplexProcessDataObjectWriteTransfer">
        /// A matching MPDO write transfer instance
        /// </param>
        /// <param name="mpdoIndex">
        /// The index of the received MPDO
        /// </param>
        /// <param name="multiplexProcessDataObjectReceiveCommunicationCobIdParameterList">
        /// List of MPDO receive communication COB-ID parameter
        /// </param>
        /// <returns>
        /// True if the received can open message is a MPDO write transfer
        /// request, false otherwise
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Will be thrown if the node id is not in the range of 0..127
        /// </exception>
        static public bool IsMultiplexProcessDataObjectWriteTransfer(CanOpenMessage receivedCanOpenMessage, byte nodeId, ICanOpenObjectDictionaryContainer canOpenObjectDictionaryContainer, out MultiplexProcessDataObjectWriteTransfer multiplexProcessDataObjectWriteTransfer, out uint mpdoIndex, IList<CanOpenUnsigned> multiplexProcessDataObjectReceiveCommunicationCobIdParameterList)
        {
            // check parameter
            if (!(0 <= nodeId && nodeId <= 127))
            {
                throw new ArgumentOutOfRangeException("nodeId", "The node id must be in the range of 0..127");
            }
            if (receivedCanOpenMessage == null || canOpenObjectDictionaryContainer == null || multiplexProcessDataObjectReceiveCommunicationCobIdParameterList == null)
            {
                multiplexProcessDataObjectWriteTransfer = null;
                mpdoIndex = 0;
                return false;
            }

            // get MPDO index
            mpdoIndex = 1000;
            foreach (CanOpenUnsigned cobIdParameter in multiplexProcessDataObjectReceiveCommunicationCobIdParameterList)
            {
                byte valid = (byte)((cobIdParameter.Value & 0x80000000) >> 31);
                if (valid == 0)
                {
                    UInt16 cobId = (UInt16)(cobIdParameter.Value & 0x1FFFFFFF);
                    UInt16 senderNodeId = receivedCanOpenMessage.CanId;
                    senderNodeId -= cobId;
                    if (1 <= senderNodeId && senderNodeId <= 127)
                    {
                        mpdoIndex = (uint)(cobIdParameter.Index - 0x1400);
                        break;
                    }
                }
            }
            if (!(0 <= mpdoIndex && mpdoIndex <= 0x1FF))
            {
                multiplexProcessDataObjectWriteTransfer = null;
                mpdoIndex = 0;
                return false;
            }

            // check the destination of the MPDO
            byte addressType = (byte)((receivedCanOpenMessage.Byte0 & 0x80) >> 7);
            if (addressType == (byte) MultiplexProcessDataObjectAdressType.DestinationAddressing)
            {
                byte address = (byte)(receivedCanOpenMessage.Byte0 & 0x7F);
                if (address != nodeId || nodeId == 0)
                {
                    multiplexProcessDataObjectWriteTransfer = null;
                    mpdoIndex = 0;
                    return false;
                }
            }

            // create transfer
            multiplexProcessDataObjectWriteTransfer = new MultiplexProcessDataObjectWriteTransfer(receivedCanOpenMessage, canOpenObjectDictionaryContainer, mpdoIndex);
            return true;
        }

        /// <summary>
        /// Starts the producer transfer
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Will be thrown if the size of the can open value object is greater
        /// than 4 bytes
        /// </exception>
        private void startProducerTransfer()
        {
            IList<byte> byteArray = CanOpenValueObject.ToByteArray();
            if (byteArray.Count > 4)
            {
                throw new ArgumentOutOfRangeException("canOpenValueObject", "The size of the can open value object to transmit must be less or equal 4 bytes");
            }
            UInt16 canId = CobId;
            canId += NodeId;
            byte type = (byte)(AddressType == MultiplexProcessDataObjectAdressType.SourceAddressing ? 0 : 1);
            byte address = (byte)(AddressType == MultiplexProcessDataObjectAdressType.SourceAddressing ? NodeId : DestinationNodeId);
            byte byte0 = (byte)((type << 7) | address);
            byte byte1 = (byte)(CanOpenValueObject.Index & 0x00FF);
            byte byte2 = (byte)((CanOpenValueObject.Index & 0xFF00) >> 8);
            byte byte3 = CanOpenValueObject.SubIndex;
            byte byte4 = (byte)(byteArray.Count > 0 ? byteArray[0] : 0);
            byte byte5 = (byte)(byteArray.Count > 1 ? byteArray[1] : 0);
            byte byte6 = (byte)(byteArray.Count > 2 ? byteArray[2] : 0);
            byte byte7 = (byte)(byteArray.Count > 3 ? byteArray[3] : 0);
            _pendingSendMessages.Add(new CanOpenMessage(canId, byte0, byte1, byte2, byte3, byte4, byte5, byte6, byte7));
            _isTransferCompleteAfterSending = true;
        }

        /// <summary>
        /// Starts the consumer transfer
        /// </summary>
        private void startConsumerTransfer()
        {
            // find matching MPDO
            CanOpenUnsigned mpdoMappingNumberObject = _canOpenObjectDictionaryContainer.GetCanOpenObject((UInt16)(0x1600 + _mpdoIndex), 0x00) as CanOpenUnsigned;
            if (mpdoMappingNumberObject == null)
            {
                TransferComplete = true;
                return;
            }
            if (mpdoMappingNumberObject.Value != 0xEF && mpdoMappingNumberObject.Value != 0xFF)
            {
                TransferComplete = true;
                return;
            }

            // process data
            List<byte> byteArray = new List<byte>();
            if (_receivedCanOpenMessage.Length > 4)
            {
                byteArray.Add(_receivedCanOpenMessage.Byte4);
            }
            if (_receivedCanOpenMessage.Length > 5)
            {
                byteArray.Add(_receivedCanOpenMessage.Byte5);
            }
            if (_receivedCanOpenMessage.Length > 6)
            {
                byteArray.Add(_receivedCanOpenMessage.Byte6);
            }
            if (_receivedCanOpenMessage.Length > 7)
            {
                byteArray.Add(_receivedCanOpenMessage.Byte7);
            }
            UInt16 index = (UInt16)(_receivedCanOpenMessage.Byte1 + (_receivedCanOpenMessage.Byte2 << 8));
            byte subIndex = _receivedCanOpenMessage.Byte3;
            ICanOpenValueObject referencedObject = _canOpenObjectDictionaryContainer.GetCanOpenObject(index, subIndex);
            if (referencedObject != null)
            {
                CanOpenAbortCode abortCode = null;
                if (!referencedObject.FromByteArray(byteArray, out abortCode))
                {
                    if (abortCode == null)
                    {
                        abortCode = new CanOpenAbortCode(0x00, 0x00, 0x00, 0x00);
                    }
                    AbortCode = abortCode;
                }
            }

            // end transfer
            TransferComplete = true;
        }


        #region ITransfer implementation
        /// <summary>
        /// Gets a flag indicating whether the transfer is complete (after
        /// sending the pending send messages)
        /// </summary>
        public bool TransferComplete
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the abort code (null for successful transfers)
        /// </summary>
        public CanOpenAbortCode AbortCode
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the pending send messages
        /// </summary>
        public ReadOnlyCollection<CanOpenMessage> PendingSendMessages
        {
            get
            {
                if (_pendingSendMessagesReference == null)
                {
                    _pendingSendMessagesReference = new ReadOnlyCollection<CanOpenMessage>(_pendingSendMessages);
                }
                return _pendingSendMessagesReference;
            }
        }


        /// <summary>
        /// Starts the transfer
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Will be thrown if this is a producer transfer and the size of the
        /// can open value object is greater than 4 bytes
        /// </exception>
        public void Start()
        {
            if (_isProducerTransfer)
            {
                startProducerTransfer();
            }
            else
            {
                startConsumerTransfer();
            }
        }

        /// <summary>
        /// Times out the transfer
        /// </summary>
        public void TimeOut()
        {
            AbortCode = new CanOpenAbortCode(0x00, 0x00, 0x04, 0x05);
        }

        /// <summary>
        /// Finalizes the transfer
        /// </summary>
        public void FinalizeTransfer()
        {
        }

        /// <summary>
        /// Continues the transfer after sending messages
        /// </summary>
        public void ContinueAfterSendingMessages()
        {
            _pendingSendMessages.Clear();
            if (_isTransferCompleteAfterSending)
            {
                TransferComplete = true;
            }
        }

        /// <summary>
        /// Processes the received message
        /// </summary>
        /// <param name="receivedCanOpenMessage">
        /// Received can open message
        /// </param>
        /// <returns>
        /// True, if the given can open message was related to this transfer,
        /// false otherwise
        /// </returns>
        public bool ProcessReceivedMessage(CanOpenMessage receivedCanOpenMessage)
        {
            return false;
        }
        #endregion ITransfer implementation

    }

}
